﻿#region 참조 네임스페이스

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Threading;

#endregion

namespace Skying.Diagnostics.Logger.Coodinator
{
	/// <summary>
	/// <see cref="Skying.Diagnostics.Logger.Entry.LogEntry"/>가 소유하며 로그 정보를 일정 패턴으로 변환하기 위해 사용된다.
	/// </summary>
	public class LogCoodinator
	{
		#region 상수

		/// <summary>
		/// 기본 로깅 패턴 상수를 나타낸다. 메시지만 출력한다.
		/// </summary>
		private const string _DEFAULT_PATTERN = "[%d{yyyy-MM-dd HH:mm:ss,fff},%s] [%tid] %m%e%S";

		/// <summary>
		/// <see cref="Skying.Diagnostics.Logger.Coodinator.LogCoodinator.Pattern"/> 로깅 패턴을 분석하기 위한 정규식 표현 상수를 나타낸다.
		/// </summary>
		private const string _REGEX_PATTERN = @"%*[\.a-zA-Z0-9\,\:\{\}\-/\s/g]*";

		#endregion

		#region 변수

		/// <summary>
		/// 분석된 <see cref="Skying.Diagnostics.Logger.Coodinator.LogCoodinator.Pattern"/>의 패턴 조각 목록을 나타낸다.
		/// </summary>
		private List<string> _patterns;

		#endregion

		#region 프로퍼티

		/// <summary>
		/// 로깅 패턴을 가져온다.
		/// </summary>
		public string Pattern
		{
			get;
			private set;
		}

		#endregion

		#region 생성자

		public LogCoodinator()
			: this(_DEFAULT_PATTERN)
		{

		}

		public LogCoodinator(string pattern)
		{
			this.Pattern = pattern;

			this._patterns = new List<string>();

			// Config 파일 상에서 패턴 문자열에 앞뒤로 빈 문자열이 있을 수도 있으므로 Trim 시킨다.
			string trimmedPattern = this.Pattern.Trim();

			try
			{
				// 정규식 표현을 사용한다.
				Regex regex = new Regex(_REGEX_PATTERN);

				// 로그 패턴에 대한 Match 목록을 검색한다.
				MatchCollection matches = regex.Matches(trimmedPattern);

				foreach (Match match in matches)
				{
					// 정규식 Match 결과가 빈 값이 아닌 경우, 패턴 조각 목록에 포함시킨다.
					if (!string.IsNullOrEmpty(match.Value.Trim()))
					{
						this._patterns.Add(match.Value);
					}
				}
			}
			catch (Exception error)
			{
				throw error;
			}
		}

		#endregion

		#region 패턴 적용

		/// <summary>
		/// <see cref="Skying.Diagnostics.Logger.Entry.LogEntry"/>가 전달하는 로그 메시지와 <see cref="System.Exception"/> 정보, 그 외 정보를 패턴에 적용한다.
		/// </summary>
		/// <param name="message">로깅할 메시지를 설정한다.</param>
		/// <param name="logSeverity">로그 심각도를 설정한다.</param>
		/// <param name="logDateTime">로깅 일시를 설정한다.</param>
		/// <param name="stackFrame">로그 발생 지점의 <see cref="System.Diagnostics.StackFrame"/> 정보를 설정한다.</param>
		/// <param name="exception">로깅할 <see cref="System.Exception"/> 정보를 설정한다. 이 파라메터는 null일 수 있다.</param>
		/// <returns>로깅할 정보가 모두 적용된 최종 로깅 메시지가 반환된다. 반환값은 <see cref="Skying.Diagnostics.Logger.Entry.LogEntry"/>의 구현체에 의해 로깅되어야 한다.</returns>
		public string Print(string message, LogSeverity logSeverity, DateTime logDateTime, StackFrame stackFrame, Exception exception = null)
		{
			string printedMessage = this.Pattern + Environment.NewLine;

			try
			{
				// 패턴에 대한 로깅 정보 사전을 생성한다.
				Dictionary<string, object> patternValueDictionary = new Dictionary<string, object>();

				// 로깅이 발생한 지점을 가져온다.
				MethodBase callerMethod = stackFrame.GetMethod();

				// 로깅 정보 사전에 로깅 정보를 적용한다.
				patternValueDictionary.Add("%severity", logSeverity.ToString());
				patternValueDictionary.Add("%s", logSeverity.ToString());
				patternValueDictionary.Add("%date", logDateTime);
				patternValueDictionary.Add("%d", logDateTime);
				patternValueDictionary.Add("%class", callerMethod.DeclaringType.FullName);
				patternValueDictionary.Add("%c", callerMethod.DeclaringType.FullName);
				patternValueDictionary.Add("%method", callerMethod.Name);
				patternValueDictionary.Add("%M", callerMethod.Name);
				patternValueDictionary.Add("%file", stackFrame.GetFileName());
				patternValueDictionary.Add("%f", stackFrame.GetFileName());
				patternValueDictionary.Add("%line", stackFrame.GetFileLineNumber());
				patternValueDictionary.Add("%l", stackFrame.GetFileLineNumber());
				patternValueDictionary.Add("%message", message);
				patternValueDictionary.Add("%m", message);
				patternValueDictionary.Add("%newline", Environment.NewLine);
				patternValueDictionary.Add("%nl", Environment.NewLine);
				patternValueDictionary.Add("%thread", Thread.CurrentThread.Name == null ? "null" : Thread.CurrentThread.Name);
				patternValueDictionary.Add("%t", Thread.CurrentThread.Name == null ? "null" : Thread.CurrentThread.Name);
				patternValueDictionary.Add("%threadId", Thread.CurrentThread.ManagedThreadId);
				patternValueDictionary.Add("%tid", Thread.CurrentThread.ManagedThreadId);

				// 예외 정보가 존재하는 경우, 로깅 정보 사전에 적용한다. 예외 정보는 앞에 NewLine을 항상 함께 표기한다.
				if (exception != null)
				{
					patternValueDictionary.Add("%exception", string.IsNullOrEmpty(exception.Message) ? string.Empty : string.Format("{0}{1}", Environment.NewLine, exception.Message));
					patternValueDictionary.Add("%e", string.IsNullOrEmpty(exception.Message) ? string.Empty : string.Format("{0}{1}", Environment.NewLine, exception.Message));
					patternValueDictionary.Add("%stacktrace", string.IsNullOrEmpty(exception.StackTrace) ? string.Empty : string.Format("{0}{1}", Environment.NewLine, exception.StackTrace));
					patternValueDictionary.Add("%S", string.IsNullOrEmpty(exception.StackTrace) ? string.Empty : string.Format("{0}{1}", Environment.NewLine, exception.StackTrace));
					patternValueDictionary.Add("%help", exception.HelpLink);
					patternValueDictionary.Add("%h", exception.HelpLink);
				}
				// 예외 정보가 존재하지 않는 경우에는 패턴에서 예외 정보와 관련된 패턴들을 제거한다.
				else
				{
					printedMessage = printedMessage.Replace("%exception", string.Empty).
						Replace("%e", string.Empty).
						Replace("%stacktrace", string.Empty).
						Replace("%S", string.Empty).
						Replace("%help", string.Empty).
						Replace("%h", string.Empty);
				}

				foreach (string pattern in this._patterns)
				{
					string patternString = pattern;
					string optionString = string.Empty;

					// 패턴 옵션이 존재하는지 여부를 검사한다.
					int index = pattern.IndexOf("{");

					// 패턴 옵션이 존재하는 경우, 패턴과 패턴 옵션을 분리해낸다.
					if (index > -1)
					{
						patternString = pattern.Substring(0, index);

						optionString = pattern.Substring(index + 1).Replace("}", string.Empty);
					}

					// 패턴이 로깅 정보 사전에 존재하지 않을 경우 무시한다.
					if (!patternValueDictionary.ContainsKey(patternString)) continue;

					object patternValue = patternValueDictionary[patternString];

					switch (patternString)
					{
						// 로깅 일시 패턴인 경우
						case "%d":
						case "%date":

							// 로깅 일시 값을 DateTime 타입으로 변환한다. 내부에서 처리되는 내용이므로 Casting 오류가 발생할 가능성은 없다.
							DateTime dateValue = (DateTime)patternValue;

							try
							{
								// 패턴 옵션은 DateTime 타입의 포맷으로 취급하여 이를 이용해 문자열로 변환한다.
								patternValue = dateValue.ToString(optionString);
							}
							catch (FormatException)
							{
								// 설정된 패턴 옵션으로 변환하다가 FormatException이 발생한 경우 기본형으로 변환한다.
								patternValue = dateValue.ToString("yyyy-MM-dd HH:mm:ss,fff");
							}

							break;
					}

					// 패턴을 로깅 정보로 치환한다.
					printedMessage = printedMessage.Replace(pattern, patternValue.ToString());
				}
			}
			catch (Exception error)
			{
				throw error;
			}

			return printedMessage;
		}

		#endregion
	}
}